<?php

namespace Dls\Entity\V0\Grid;

use Illuminate\Database\Eloquent\Model as EloquentModel;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;

class Model
{
    /**
     * Eloquent model instance of the grid model.
     *
     * @var EloquentModel|Builder
     */
    protected $model;

    /**
     * Array of queries of the eloquent model.
     *
     * @var \Illuminate\Support\Collection
     */
    protected $queries;

    /**
     * Sort parameters of the model.
     *
     * @var array
     */
    protected $sort;

    /**
     * @var Collection
     */
    protected $data = [];

    /*
     * 20 items per page as default.
     *
     * @var int
     */
    protected $perPage = 20;

    /**
     * If the model use pagination.
     *
     * @var bool
     */
    protected $usePaginate = true;

    /**
     * The query string variable used to store the per-page.
     *
     * @var string
     */
    protected $perPageName = 'pageNum';

    /**
     * Collection callback.
     *
     * @var \Closure
     */
    protected $collectionCallback;
    /**
     * Collection
     *
     * @var Collection
     */
    protected $columns;

    /**
     * Create a new grid model instance.
     *
     * @param EloquentModel $model
     */
    public function __construct(EloquentModel $model)
    {
        $this->model = $model;

        $this->queries = collect();
        $this->columns = collect();

//        static::doNotSnakeAttributes($this->model);
    }

    /**
     * Don't snake case attributes.
     *
     * @param EloquentModel $model
     *
     * @return void
     */
    protected static function doNotSnakeAttributes(EloquentModel $model)
    {
        $class = get_class($model);

        $class::$snakeAttributes = false;
    }

    /**
     * Get the eloquent model of the grid model.
     *
     * @return EloquentModel
     */
    public function eloquent()
    {
        return $this->model;
    }

    /**
     * Enable or disable pagination.
     *
     * @param bool $use
     */
    public function usePaginate($use = true)
    {
        $this->usePaginate = $use;
        return $this;
    }

    /**
     * Get the query string variable used to store the per-page.
     *
     * @return string
     */
    public function getPerPageName()
    {
        return $this->perPageName;
    }

    /**
     * Set the query string variable used to store the per-page.
     *
     * @param string $name
     *
     * @return $this
     */
    public function setPerPageName($name)
    {
        $this->perPageName = $name;

        return $this;
    }

    /**
     * Set collection callback.
     *
     * @param \Closure $callback
     *
     * @return $this
     */
    public function collection(\Closure $callback = null)
    {
        $this->collectionCallback = $callback;

        return $this;
    }

    /**
     * Build.
     *
     * @return array|Collection
     */
    public function buildData($result, $toArray = true)
    {
        $ret = new Collection();
        if (empty($this->data)) {
            // 如果是分页
            if ($result instanceof LengthAwarePaginator) {
                $this->data = $result->items();
            } else {
                $this->data = $result;
            }

            // run row handle
            if ($this->collectionCallback) {
                $this->data = call_user_func($this->collectionCallback, $this->data);
            }

            // run single field process
            foreach ($this->data as $datum) {
                $tmp = [];
                foreach ($this->columns as $column) {
                    if ($column->hasDisplay()) {
                        $tmp[$column->getAlias()] = $column->with($datum[$column->getAlias()]);
                    } else {
                        $tmp[$column->getAlias()] = $datum[$column->getAlias()];
                    }
                }
                $ret->push($tmp);
            }
            unset($datum, $this->data); // 释放空间
        }
        return $toArray ? $ret->toArray() : $ret;
    }

    /**
     * @param callable $callback
     * @param int $count
     *
     * @return bool
     */
    public function chunk($callback, $count = 100)
    {
        if ($this->usePaginate) {
            $this->model->chunk($count, $callback);
        }

        $this->queries->reject(function ($query) {
            return $query['method'] == 'paginate';
        })->each(function ($query) {
            $this->model = $this->model->{$query['method']}(...$query['arguments']);
        });
        return $this->buildData($this->get(), false)->chunk($count)->each($callback);
    }

    /**
     * Get table of the model.
     *
     * @return string
     */
    public function getTable()
    {
        return $this->model->getTable();
    }

    /**
     *  run select, where, groupBy, sort functions
     *  return a collection or paginator
     *
     * @return LengthAwarePaginator | Collection
     */
    public function get()
    {
        // 字段
        $this->rawColumn();
        // 搜索
        $this->setSearcher();
        // 分组
        $this->setGroupBy();
        // 排序
        $this->setSortable();
        // 唯一 exec 的方法
        $this->execQuery();

        return $this->usePaginate ?
            $this->getPaginator() :
            $this->all();
    }

    /**
     * 获取全部
     *
     * @return Collection
     */
    protected function all()
    {
        return $this->model->get();
    }

    /**
     * make select field can use \DB::raw()
     * and push the select function to queries
     */
    protected function rawColumn()
    {

        $select = $this->columns->reject(function (Column $column) {
            return $column->getIgnore();
        })->filter(function (Column $column) {
            return strpos($column->getName(), '.') === false;
        })->map(function (Column $column) {
            if ($formatter = $column->getFormatter()) {
                return \DB::raw($formatter->formatter($column));
            } else {
                return $column->getAlias() ? "{$column->getName()} AS {$column->getAlias()}" : $column->getName();
            }
        })->toArray();

        $query = [
            'method'    => 'select',
            'arguments' => $select
        ];

        $this->queries->push($query);
    }

    /**
     * todo 关联表查询
     */
    protected function setSearcher()
    {
        $this->columns->each(function (Column $column) {
            if ($searcher = $column->getSearcher()) {
                $this->model = $searcher->bind($column)->search($this->model->newQuery());
            }
        });
    }

    /**
     *  set the group by field to the method groupBy as the parameter
     *
     */
    protected function setGroupBy()
    {
        $groupBy = $this->columns->map(function (Column $column) {
            if ($column->getGroupBy()) {
                return $column->getName();
            }
        })->filter()->toArray();

        if (empty($groupBy)) {
            return;
        }

        $query = [
            'method'    => 'groupBy',
            'arguments' => $groupBy,
        ];

        $this->queries->push($query);
    }

    protected function execQuery()
    {
        // exec method
        $this->queries->unique()->each(function ($query) {
            $this->model = call_user_func_array([$this->model, $query['method']], $query['arguments']);
        });
    }

    /**
     * 支持 group by的分页
     *
     * @return LengthAwarePaginator
     */
    public function getPaginator()
    {
        $perPage = app('request')->input($this->perPageName);
        $page    = Paginator::resolveCurrentPage() - 1;

        $bool  = $this->queries->search(function ($item) {
            return $item['method'] === 'groupBy';
        });
        $total = $bool === false ? $this->model->count() : $this->runGroupByPaginationCountQuery();
        $items = $this->model->skip($page * $perPage)->take($perPage)->get();

        return new LengthAwarePaginator($items, $total, $perPage);
    }

    public function clearOnlyWhere()
    {
        $properties = ['columns', 'orders', 'limit', 'offset'];
        $except     = ['select', 'order'];
        $clone = clone $this->model;
        return $clone->newQuery()
            ->tap(function (\Illuminate\Database\Eloquent\Builder $builder) use ($properties) {
                foreach ($properties as $property) {
                    $builder->getQuery()->{$property} = null;
                }
            })->tap(function (\Illuminate\Database\Eloquent\Builder $builder) use ($except) {
                foreach ($except as $type) {
                    $builder->getQuery()->bindings[$type] = [];
                }
            });

    }

    private function runGroupByPaginationCountQuery()
    {

        return $this->clearOnlyWhere()
            ->select(\DB::raw('COUNT(1)'))
            ->get()
            ->count();
    }

    /**
     * Find query by method name.
     *
     * @param $method
     *
     * @return array
     */
    protected function findQueryByMethod($method)
    {
        return $this->queries->first(function ($query) use ($method) {
            return $query['method'] == $method;
        });
    }

    /**
     * Set the grid sort.
     *
     * @return void
     */
    protected function setSortable()
    {
        if (($querySort = $this->parseQuerySort())) {
            $sort = $querySort;
        } elseif (($columnSort = $this->parseColumnSort())) {
            $sort = $columnSort;
        } else {
            return;
        }

        foreach ($sort as $item) {
            $this->model = $this->model->orderBy($item['column'], $item['direction']);
        }
    }

    /**
     * parse sort in columns
     *
     * @return Collection
     */
    private function parseColumnSort()
    {
        return $this->columns->map(function (Column $column) {
            if ($tmp = $column->getSort()) {
                return $tmp;
            }
        })->filter();
    }

    /**
     * parse sort query parameters
     *
     * @return array
     */
    private function parseQuerySort()
    {
        $sort    = [];
        $sortStr = request()->input('sort');
        if (!empty($sortStr)) {
            foreach (explode(',', $sortStr) as $group) {
                $tmp       = [];
                $field     = trim($group);
                $direction = $group[0] === '-' ? 'desc' : 'asc';
                if ($direction === 'desc') {
                    $field = substr($group, 1);
                }
                $tmp['column']    = $field;
                $tmp['direction'] = $direction;
            }
            $sort[] = $tmp; // 装载排序的数组
        }
        return $sort;
    }

    /**
     * todo 管理关系的排序
     * Set relation sort.
     *
     * @param string $column
     *
     * @return void
     */
    protected function setRelationSort($column)
    {
        list($relationName, $relationColumn) = explode('.', $column);

        if ($this->queries->contains(function ($query) use ($relationName) {
            return $query['method'] == 'with' && in_array($relationName, $query['arguments']);
        })) {
            $relation = $this->model->$relationName();

            $this->queries->push([
                'method'    => 'join',
                'arguments' => $this->joinParameters($relation),
            ]);

            $this->resetOrderBy();

            $this->queries->push([
                'method'    => 'orderBy',
                'arguments' => [
                    $relation->getRelated()->getTable() . '.' . $relationColumn,
                    $this->sort['type'],
                ],
            ]);
        }
    }

    /**
     * Reset orderBy query.
     *
     * @return void
     */
    public function resetOrderBy()
    {
        $this->queries = $this->queries->reject(function ($query) {
            return $query['method'] == 'orderBy' || $query['method'] == 'orderByDesc';
        });
    }

    /**
     * Build join parameters for related model.
     *
     * `HasOne` and `BelongsTo` relation has different join parameters.
     *
     * @param Relation $relation
     *
     * @throws \Exception
     *
     * @return array
     */
    protected function joinParameters(Relation $relation)
    {
        $relatedTable = $relation->getRelated()->getTable();

        if ($relation instanceof BelongsTo) {
            return [
                $relatedTable,
                $relation->getForeignKey(),
                '=',
                $relatedTable . '.' . $relation->getRelated()->getKeyName(),
            ];
        }

        if ($relation instanceof HasOne) {
            return [
                $relatedTable,
                $relation->getQualifiedParentKeyName(),
                '=',
                $relation->getQualifiedForeignKeyName(),
            ];
        }

        throw new \Exception('Related sortable only support `HasOne` and `BelongsTo` relation.');
    }

    /**
     * set columns on grid
     *
     * @param Collection $columns
     */
    public function setColumn($column)
    {
        $this->columns = $column;
    }

    /**
     * @param string $method
     * @param array $arguments
     *
     * @return $this
     */
    public function __call($method, $arguments)
    {
        $this->queries->push([
            'method'    => $method,
            'arguments' => $arguments,
        ]);

        return $this;
    }

    /**
     * @param $key
     *
     * @return mixed
     */
    public function __get($key)
    {
        $data = $this->buildData($this->get());

        if (array_key_exists($key, $data)) {
            return $data[$key];
        }
    }
}
